package za.ac.sun.cs.distrib.logging;

import java.io.BufferedReader;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;
import java.util.Date;
import java.util.List;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

import org.apfloat.Apint;

import redis.clients.jedis.Jedis;

public class LogHttpServer {

	private static final int DEFAULT_PORT = 8000;

	private static final String DEFAULT_REDIS_HOST = "localhost";

	private static final int DEFAULT_REDIS_PORT = 7711;

	private static final boolean DEFAULT_PRODUCE_OUTPUT = true;
	
	public static void main(String[] args) {
		int pp = DEFAULT_PORT;
		String h = DEFAULT_REDIS_HOST;
		int p = DEFAULT_REDIS_PORT;
		boolean po = DEFAULT_PRODUCE_OUTPUT;
		int i = 0;
		while (i < args.length) {
			String o = args[i++];
			if (o.equals("--output")) {
				po = true;
			} else if (o.equals("--no-output")) {
				po = false;
			} else if (o.equals("--port")) {
				if (i < args.length) {
					try {
						pp = Integer.parseInt(args[i++]);
					} catch (NumberFormatException x) {
						System.err.println("Illegal port number ignored");
					}
				} else {
					System.err.println("Missing port number");
				}
			} else if (o.startsWith("--port=")) {
				try {
					pp = Integer.parseInt(o.substring(7));
				} catch (NumberFormatException x) {
					System.err.println("Illegal port number ignored");
				}
			} else if (o.equals("--redishost")) {
				if (i < args.length) {
					h = args[i++];
				} else {
					System.err.println("Missing host name");
				}
			} else if (o.startsWith("--redishost=")) {
				h = o.substring(12);
			} else if (o.equals("--redisport")) {
				if (i < args.length) {
					try {
						p = Integer.parseInt(args[i++]);
					} catch (NumberFormatException x) {
						System.err.println("Illegal port number ignored");
					}
				} else {
					System.err.println("Missing port number");
				}
			} else if (o.startsWith("--redisport=")) {
				try {
					p = Integer.parseInt(o.substring(12));
				} catch (NumberFormatException x) {
					System.err.println("Illegal port number ignored");
				}
			} else {
				System.err.println("Unrecognized option \"" + o + "\"");
			}
		}
		new LogHttpServer(pp, h, p, po).run();
	}

	private final String UPDATING_HEADER = new StringBuilder()
			.append("<!DOCTYPE html PUBLIC \"-//W3C//DTD HTML 3.2 Final//EN\">\r\n")
			.append("<html><head>\r\n")
			.append("<title>LogHttpServer</title>\r\n")
			.append("<meta http-equiv=refresh content='5'>\r\n")
			.append("</head><body>\r\n").toString();

	private final String NONUPDATING_HEADER = new StringBuilder()
			.append("<!DOCTYPE html PUBLIC \"-//W3C//DTD HTML 3.2 Final//EN\">\r\n")
			.append("<html><head>\r\n")
			.append("<title>LogHttpServer</title>\r\n")
			.append("</head><body>\r\n").toString();

	private final String FOOTER = new StringBuilder()
			.append("</body></html>\r\n").toString();

	private final int port;

	private final Jedis db;

	private final boolean produceOutput;
	
	private ServerSocket serverSocket;

	private int requestCount = 0;

	private int zipRequestCount = 0;
	
	private int nullRequestCount = 0;

	public LogHttpServer(int port, String redisHost, int redisPort, boolean produceOutput) {
		this.port = port;
		db = new Jedis(redisHost, redisPort);
		this.produceOutput = produceOutput;
	}

	private void println(String message) {
		if (produceOutput) {
			System.out.println(message);
		}
	}

	private void run() {
		println("HTTP server started");
		try {
			serverSocket = new ServerSocket(port);
			while (true) {
				Socket socket = serverSocket.accept();
				InputStreamReader r = new InputStreamReader(socket.getInputStream());
				BufferedReader input = new BufferedReader(r);
				String request = input.readLine();
				if (request == null) {
					println("Null request -- ignoring");
					nullRequestCount++;
				} else {
					requestCount++;
					DataOutputStream output = new DataOutputStream(socket.getOutputStream());
					processRequest(output, request);
				}
				socket.close();
			}
		} catch (SocketException x) {
			x.printStackTrace();
		} catch (IOException x) {
			x.printStackTrace();
			return;
		} finally {
			println("HTTP server stopping");
			try {
				serverSocket.close();
			} catch (IOException x) {
				x.printStackTrace();
			}
		}
	}

	private void processRequest(DataOutputStream output, String request) {
		String[] parts = request.split(" ");
		if ((parts.length == 3) && (parts[0].equals("GET"))) {
			if (parts[1].equals("/")) {
				processRootRequest(output);
			} else if (parts[1].equals("/info")) {
				processInfoRequest(output);
			} else if (parts[1].endsWith(".zip")) {
				processZipRequest(output, parts[1]);
				zipRequestCount++;
			} else {
				processLogRequest(output, parts[1].substring(1));
			}
		} else {
			fourZeroZero(output);
		}
	}

	private void processRootRequest(DataOutputStream output) {
		final String directory = System.getProperty("java.io.tmpdir");
		StringBuilder b = new StringBuilder().append(NONUPDATING_HEADER);
		b.append("<h1>Available logs</h1>\r\n");
		b.append("<ul>\r\n");
		Set<String> logs = db.keys("log:*");
		for (String log : logs) {
			String name = log.substring(4);
			b.append("<li>");
			b.append("<a href=\"/").append(name);
			b.append("\">").append(name).append("</a>");
			String filename = String.format("%s/%s.zip", directory, name);
			File file = new File(filename);
			if (file.exists()) {
				b.append("&nbsp;<a href=\"").append(filename);
				b.append("\">").append(filename).append("</a>");
			}
			b.append("</li>\r\n");
		}
		b.append("</ul>\r\n");
		send(output, b.append(FOOTER).toString());
	}

	private void processLogRequest(DataOutputStream output, String key) {
		StringBuilder b = new StringBuilder().append(UPDATING_HEADER);
		b.append("<h1>").append(key).append("</h1>");
		List<String> sublogs = db.lrange("log:" + key, 0, -1);
		if (sublogs.size() == 0) {
			FourZeroFour(output);
			return;
		}
		long minTimestamp = -1;
		long maxTimestamp = -1;
		b.append("<table border=\"1\"><tr>\r\n");
		b.append("<th>host</th>\r\n");
		b.append("<th>pid</th>\r\n");
		b.append("<th>paths</th>\r\n");
		b.append("<th>values</th>\r\n");
		b.append("<th>full</th>\r\n");
		b.append("<th>overlaps</th>\r\n");
		b.append("<th>updates</th>\r\n");
		b.append("<th>choices</th>\r\n");
		b.append("<th>deadends</th>\r\n");
		b.append("<th>cputime</th>\r\n");
		Apint totalPaths = Apint.ZERO;
		Apint totalValues = Apint.ZERO;
		Apint totalFullPaths = Apint.ZERO;
		Apint totalOverlaps = Apint.ZERO;
		Apint totalUpdates = Apint.ZERO;
		Apint totalChoices = Apint.ZERO;
		Apint totalDeadends = Apint.ZERO;
		double totalCPU = 0;
		SortedMap<String, String> rows = new TreeMap<String, String>();
		int activeCount = rows.size();
		for (String sublog : sublogs) {
			StringBuilder c = new StringBuilder();
			if (db.exists(sublog + ":done")) {
				c.append("</tr><tr bgcolor=\"#ff9999\">\r\n");
			} else {
				c.append("</tr><tr bgcolor=\"#99ff99\">\r\n");
				activeCount--;
			}
			String identify = db.get(sublog + ":0");
			if (identify == null) {
				b.append("<td align=\"left\" colspan=\"10\">done</td>\r\n");
			} else {
				String[] fields = identify.split("@@");
				long timestamp = Long.parseLong(fields[0]);
				if ((minTimestamp == -1) || (timestamp < minTimestamp)) {
					minTimestamp = timestamp;
				}
				if ((maxTimestamp == -1) || (timestamp > maxTimestamp)) {
					maxTimestamp = timestamp;
				}
				String ip = fields[2];
				String id = fields[3];
				if (id.contains("@")) {
					String[] subfields = id.split("@");
					id = subfields[0];
					ip = subfields[1];
				}
				String stats = db.get(sublog + ":stats");
				Apint numPaths;
				Apint numValues;
				Apint numFullPaths;
				Apint numOverlaps;
				Apint numUpdates;
				Apint numChoices;
				Apint numDeadends;
				double cpuTime;
				if (stats == null) {
					numPaths = Apint.ZERO;
					numValues = Apint.ZERO;
					numFullPaths = Apint.ZERO;
					numOverlaps = Apint.ZERO;
					numUpdates = Apint.ZERO;
					numChoices = Apint.ZERO;
					numDeadends = Apint.ZERO;
					cpuTime = 0.0;
				} else {
					fields = stats.split("@@");
					timestamp = Long.parseLong(fields[0]);
					if ((minTimestamp == -1) || (timestamp < minTimestamp)) {
						minTimestamp = timestamp;
					}
					if ((maxTimestamp == -1) || (timestamp > maxTimestamp)) {
						maxTimestamp = timestamp;
					}
					numPaths = new Apint(fields[2]);
					numValues = new Apint(fields[3]);
					numOverlaps = new Apint(fields[4]);
					numFullPaths = numPaths.subtract(numOverlaps);
					numUpdates = new Apint(fields[5]);
					numChoices = new Apint(fields[6]);
					numDeadends = new Apint(fields[7]);
					cpuTime = Long.parseLong(fields[8]) * 0.000001;
				}
				totalPaths = totalPaths.add(numPaths);
				totalValues = totalValues.add(numValues);
				totalOverlaps = totalOverlaps.add(numOverlaps);
				totalFullPaths = totalFullPaths.add(numFullPaths);
				totalUpdates = totalUpdates.add(numUpdates);
				totalChoices = totalChoices.add(numChoices);
				totalDeadends = totalDeadends.add(numDeadends);
				totalCPU += cpuTime;
				c.append("<td align=\"right\">").append(ip).append("</td>\r\n");
				c.append("<td align=\"right\">").append(id).append("</td>\r\n");
				c.append("<td align=\"right\">").append(numPaths).append("</td>\r\n");
				c.append("<td align=\"right\">").append(numValues).append("</td>\r\n");
				c.append("<td align=\"right\">").append(numFullPaths).append("</td>\r\n");
				c.append("<td align=\"right\">").append(numOverlaps).append("</td>\r\n");
				c.append("<td align=\"right\">").append(numUpdates).append("</td>\r\n");
				c.append("<td align=\"right\">").append(numChoices).append("</td>\r\n");
				c.append("<td align=\"right\">").append(numDeadends).append("</td>\r\n");
				c.append("<td align=\"right\">").append(String.format("%.2f", cpuTime)).append("</td>\r\n");
				rows.put(ip + "::" + id, c.toString());
			}
		}
		for (String k : rows.keySet()) {
			b.append(rows.get(k));
		}
		b.append("</tr><tr>\r\n");
		b.append("<th align=\"right\" colspan=\"2\">Total</th>\r\n");
		b.append("<th align=\"right\">").append(totalPaths).append("</th>\r\n");
		b.append("<th align=\"right\">").append(totalValues).append("</th>\r\n");
		b.append("<th align=\"right\">").append(totalFullPaths).append("</th>\r\n");
		b.append("<th align=\"right\">").append(totalOverlaps).append("</th>\r\n");
		b.append("<th align=\"right\">").append(totalUpdates).append("</th>\r\n");
		b.append("<th align=\"right\">").append(totalChoices).append("</th>\r\n");
		b.append("<th align=\"right\">").append(totalDeadends).append("</th>\r\n");
		b.append("<th align=\"right\">").append(String.format("%.2f", totalCPU)).append("</th>\r\n");
		b.append("</tr></table>\r\n");
		b.append("<p>\r\n");
		double runtime = (maxTimestamp - minTimestamp) * 0.001;
		b.append("Runtime: ").append(String.format("%.2f", runtime)).append(" ms\r\n");
		b.append("<br/>Current: ").append(new Date()).append("\r\n");
		b.append("</p>\r\n");
		send(output, b.append(FOOTER).toString());
		if (activeCount == 0) {
			createZipFile(key);
		}
	}

	private void createZipFile(String key) {
		String directory = System.getProperty("java.io.tmpdir");
		String filename = String.format("%s/%s.zip", directory, key);
		File file = new File(filename);
		if (file.exists()) {
			return;
		}
		List<String> sublogs = db.lrange("log:" + key, 0, -1);
		if (sublogs.size() == 0) {
			return;
		}
		try {
			FileOutputStream output = new FileOutputStream(file);
			ZipOutputStream zip = new ZipOutputStream(output);
			for (String sublog : sublogs) {
				zip.putNextEntry(new ZipEntry(sublog + ".txt"));
				int counter = 0;
				String entry = db.get(sublog + ":" + counter);
				while (entry != null) {
					byte[] b = (entry + "\n").getBytes();
					zip.write(b, 0, b.length);
					counter++;
					entry = db.get(sublog + ":" + counter);
				}
				zip.closeEntry();
			}
			output.flush();
			zip.close();
			for (String sublog : sublogs) {
				int counter = 1;
				String k = sublog + ":" + counter;
				String entry = db.get(k);
				while (entry != null) {
					db.del(k);
					counter++;
					k = sublog + ":" + counter;
					entry = db.get(k);
				}
			}
		} catch (FileNotFoundException x) {
			x.printStackTrace();
		} catch (IOException x) {
			x.printStackTrace();
		}
	}

	private void processZipRequest(DataOutputStream output, String filename) {
		File file = new File(filename);
		if (!file.exists()) {
			FourZeroFour(output);
			return;
		}
		try {
			byte[] bytes = new byte[(int) file.length()];
			FileInputStream input = new FileInputStream(file);
			input.read(bytes);
			input.close();
			output.writeBytes("HTTP/1.0 200 OK\r\n");
			output.writeBytes("Server: LogHttpServer\r\n");
			output.writeBytes("Content-type: application/x-gzip\r\n");
			output.writeBytes("Content-Length: " + bytes.length + "\r\n");
			output.writeBytes("\r\n");
			output.write(bytes);
			output.flush();
			output.close();
		} catch (IOException x) {
			x.printStackTrace();
			fourZeroZero(output);
		}
	}
	
	private void processInfoRequest(DataOutputStream output) {
		StringBuilder b = new StringBuilder().append(NONUPDATING_HEADER);
		b.append("<h1>LogHttpServer information</h1>");
		b.append("<pre>");
		b.append("\nrequestCount==").append(requestCount);
		b.append("\nzipRequestCount==").append(zipRequestCount);
		b.append("\nnullRequestCount==").append(nullRequestCount);
		b.append("\n</pre>");
		send(output, b.append(FOOTER).toString());
	}

	private void fourZeroZero(DataOutputStream output) {
		StringBuilder b = new StringBuilder();
		b.append("<!DOCTYPE HTML PUBLIC \"-//IETF//DTD HTML 2.0//EN\">\r\n");
		b.append("<html><head>\r\n");
		b.append("<title>400 Bad Request</title>\r\n");
		b.append("</head><body>\r\n");
		b.append("<h1>Bad Request</h1>\r\n");
		b.append("<p>Your browser sent a request that this server could not understand.</p>\r\n");
		b.append("</body></html>\r\n");
		try {
			output.writeBytes(b.toString());
		} catch (IOException x) {
			x.printStackTrace();
		}
	}

	private void FourZeroFour(DataOutputStream output) {
		StringBuilder b = new StringBuilder();
		b.append("<!DOCTYPE HTML PUBLIC \"-//IETF//DTD HTML 2.0//EN\">\r\n");
		b.append("<html><head>\r\n");
		b.append("<title>404 Not Found</title>\r\n");
		b.append("</head><body>\r\n");
		b.append("<h1>Not Found</h1>\r\n");
		b.append("<p>The requested URL /jindex.html was not found on this server.</p>\r\n");
		b.append("</body></html>\r\n");
		send(output, "HTTP/1.1 404 Not Found", b.toString());
	}

	private void send(DataOutputStream output, String content) {
		send(output, "HTTP/1.0 200 OK", content);
	}

	private void send(DataOutputStream output, String response, String content) {
		try {
			output.writeBytes(response + "\r\n");
			output.writeBytes("Server: LogHttpServer\r\n");
			output.writeBytes("Content-type: text/html; charset=utf-8\r\n");
			output.writeBytes("Content-Length: " + content.length() + 4 + "\r\n");
			output.writeBytes("\r\n");
			output.writeBytes(content);
			output.writeBytes("\r\n");
			output.close();
		} catch (IOException x) {
			x.printStackTrace();
		}
	}
	
}
